/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is Forte for Java, Community Edition. The Initial * Developer of the Original Code is Sun Microsystems, Inc. Portions * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved. */ package org.netbeans.editor; import java.io.Reader; import java.io.Writer; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.List; import java.util.Iterator; import javax.swing.text.BadLocationException; import javax.swing.text.Segment; /** * Various text analyzes over the document * * @author Miloslav Metelka * @version 1.00 */ public class Analyzer { /** Platform default line separator */ private static Object platformLS; /** Empty char array */ public static final char[] EMPTY_CHAR_ARRAY = new char[0]; /** Buffer filled by spaces used for spaces filling and tabs expansion */ private static char spacesBuffer[] = new char[] { ' ' }; /** Buffer filled by tabs used for tabs filling */ private static char tabsBuffer[] = new char[] { '\t' }; private Analyzer() { // no instantiation } /** Get platform default line separator */ public static Object getPlatformLS() { if (platformLS == null) { platformLS = System.getProperty("line.separator"); // NOI18N } return platformLS; } /** Test line separator on given semgment. This implementation simply checks * the first line of file but it can be redefined to do more thorough test. * @param seg segment where analyzes are performed * @return line separator type found in the file */ public static String testLS(char chars[], int len) { for (int i = 0; i < len; i++) { switch (chars[i]) { case '\r': if (i + 1 < len && chars[i + 1] == '\n') { return BaseDocument.LS_CRLF; } else { return BaseDocument.LS_CR; } case '\n': return BaseDocument.LS_LF; } } return null; // signal unspecified line separator } /** Convert text with generic line separators to line feeds (LF). * As the linefeeds are one char long there is no need to allocate * another buffer since the only possibility is that the returned * length will be smaller than previous (if there were some CRLF separators. * @param chars char array with data to convert * @param len valid portion of chars array * @return new valid portion of chars array after conversion */ public static int convertLSToLF(char chars[], int len) { int tgtOffset = 0; short lsLen = 0; // length of separator found int moveStart = 0; // start of block that must be moved int moveLen; // length of data moved back in buffer for (int i = 0; i < len; i++) { // first of all - there's no need to handle single '\n' if (chars[i] == '\r') { // '\r' found if (i + 1 < len && chars[i + 1] == '\n') { // '\n' follows lsLen = 2; // '\r\n' } else { lsLen = 1; // only '\r' } } if (lsLen > 0) { moveLen = i - moveStart; if (moveLen > 0) { if (tgtOffset != moveStart) { // will need to arraycopy System.arraycopy(chars, moveStart, chars, tgtOffset, moveLen); } tgtOffset += moveLen; } chars[tgtOffset++] = '\n'; moveStart += moveLen + lsLen; // skip separator i += lsLen - 1; // possibly skip '\n' lsLen = 0; // signal no separator found } } // now move the rest if it's necessary moveLen = len - moveStart; if (moveLen > 0) { if (tgtOffset != moveStart) { System.arraycopy(chars, moveStart, chars, tgtOffset, moveLen); } tgtOffset += moveLen; } return tgtOffset; // return current length } /** Convert string with generic line separators to line feeds (LF). * @param text string to convert * @return new string with converted LSs to LFs */ public static String convertLSToLF(String text) { char[] tgtChars = null; int tgtOffset = 0; short lsLen = 0; // length of separator found int moveStart = 0; // start of block that must be moved int moveLen; // length of data moved back in buffer int textLen = text.length(); for (int i = 0; i < textLen; i++) { // first of all - there's no need to handle single '\n' if (text.charAt(i) == '\r') { // '\r' found if (i + 1 < textLen && text.charAt(i + 1) == '\n') { // '\n' follows lsLen = 2; // '\r\n' } else { lsLen = 1; // only '\r' } } if (lsLen > 0) { if (tgtChars == null) { tgtChars = new char[textLen]; text.getChars(0, textLen, tgtChars, 0); // copy whole array } moveLen = i - moveStart; if (moveLen > 0) { if (tgtOffset != moveStart) { // will need to arraycopy text.getChars(moveStart, moveStart + moveLen, tgtChars, tgtOffset); } tgtOffset += moveLen; } tgtChars[tgtOffset++] = '\n'; moveStart += moveLen + lsLen; // skip separator i += lsLen - 1; // possibly skip '\n' lsLen = 0; // signal no separator found } } // now move the rest if it's necessary moveLen = textLen - moveStart; if (moveLen > 0) { if (tgtOffset != moveStart) { text.getChars(moveStart, moveStart + moveLen, tgtChars, tgtOffset); } tgtOffset += moveLen; } return (tgtChars == null) ? text : new String(tgtChars, 0, tgtOffset); } public static boolean isSpace(String s) { int len = s.length(); for (int i = 0; i < len; i++) { if (s.charAt(i) != ' ') { return false; } } return true; } public static boolean isSpace(char[] chars) { return isSpace(chars, 0, chars.length); } /** Return true if the array contains only space chars */ public static boolean isSpace(char[] chars, int offset, int len) { while (len > 0) { if (chars[offset++] != ' ') { return false; } len--; } return true; } public static boolean isWhitespace(char[] chars) { return isWhitespace(chars, 0, chars.length); } /** Return true if the array contains only space or tab chars */ public static boolean isWhitespace(char[] chars, int offset, int len) { while (len > 0) { if (!Character.isWhitespace(chars[offset])) { return false; } offset++; len--; } return true; } public static int getFirstNonSpace(char[] chars) { return getFirstNonSpace(chars, 0, chars.length); } /** Return the first index that is not space */ public static int getFirstNonSpace(char[] chars, int offset, int len) { while (len > 0) { if (chars[offset] != ' ') { return offset; } offset++; len--; } return -1; } public static int getFirstNonWhite(char[] chars) { return getFirstNonWhite(chars, 0, chars.length); } /** Return the first index that is not space or tab or new-line char */ public static int getFirstNonWhite(char[] chars, int offset, int len) { while (len > 0) { switch (chars[offset]) { case ' ': case '\t': case '\n': break; default: return offset; } offset++; len--; } return -1; } public static int getLastNonWhite(char[] chars) { return getLastNonWhite(chars, 0, chars.length); } /** Return the last index that is not space or tab or new-line char */ public static int getLastNonWhite(char[] chars, int offset, int len) { int i = offset + len - 1; while (i >= offset) { switch (chars[i]) { case ' ': case '\t': case '\n': break; default: return i; } i--; } return -1; } /** Count the number of line feeds in char array. * @return number of LF characters contained in array. */ public static int getLFCount(char chars[]) { return getLFCount(chars, 0, chars.length); } public static int getLFCount(char chars[], int offset, int len) { int lfCnt = 0; while (len > 0) { if (chars[offset++] == '\n') { lfCnt++; } len--; } return lfCnt; } public static int getLFCount(String s) { int lfCount = 0; int len = s.length(); for (int i = 0; i < len; i++) { if (s.charAt(i) == '\n') { lfCount++; } } return lfCount; } public static int getFirstLFOffset(String s) { int len = s.length(); for (int i = 0; i < len; i++) { if (s.charAt(i) == '\n') { return i; } } return -1; } /** Get offset of the first found new-line * @return offset of '\n' or -1 */ public static int getFirstLFOffset(char[] chars) { return getFirstLFOffset(chars, 0, chars.length); } public static int getFirstLFOffset(char[] chars, int offset, int len) { while (len > 0) { if (chars[offset++] == '\n') { return offset - 1; } len--; } return -1; } public static int getFirstTab(char[] chars) { return getFirstTab(chars, 0, chars.length); } public static int getFirstTab(char[] chars, int offset, int len) { while (len > 0) { if (chars[offset++] == '\t') { return offset - 1; } len--; } return -1; } /** Reverses the order of characters in the array. It works from * the begining of the array, so no offset is given. */ public static void reverse(char[] chars, int len) { for (int i = ((--len - 1) >> 1); i >= 0; --i) { char ch = chars[i]; chars[i] = chars[len - i]; chars[len - i] = ch; } } public static boolean equals(String s, char[] chars) { return equals(s, chars, 0, chars.length); } public static boolean equals(String s, char[] chars, int offset, int len) { if (s.length() != len) { return false; } for (int i = 0; i < len; i++) { if (s.charAt(i) != chars[offset + i]) { return false; } } return true; } /** Do initial reading of document. Translate any line separators * found in document to line separators used by document. It also cares * for elements that were already created on the empty document. Although * the document must be empty there can be already marks created. Initial * read is equivalent to inserting the string array of the whole document * size at position 0 in the document. Therefore all the marks that are * not insertAfter are removed and reinserted to the end of the document * after the whole initial read is finished. * @param doc document for which the initialization is performed * @param reader reader from which document should be read * @param lsType line separator type * @param testLS test line separator of file and if it's consistent, use it * @param markDistance the distance between the new syntax mark is put */ public static void initialRead(BaseDocument doc, Reader reader, boolean testLS) throws IOException { // document must be empty if (doc.getLength() > 0) { return; // no read when already initialized } // for valid reader read the document if (reader != null) { int readBufferSize = ((Integer)doc.getProperty( Settings.READ_BUFFER_SIZE)).intValue(); int markDistance = ((Integer)doc.getProperty( Settings.READ_MARK_DISTANCE)).intValue(); Syntax syntax = doc.createSyntax(); /* buffer into which the data from file will be read */ char readBuf[] = new char[readBufferSize + 2]; boolean firstRead = true; // first cycle of reading from stream boolean lastRead = false; // last cycle of reading from stream /* Was the last char in previous buffer '\r'? This should be * naturally boolean, but integer helps better in calculations. */ int lastCR = 0; /* The same flag as previous one for currently read buffer */ int thisCR = 0; int readLen = 0; // how many chars was read within cycle int bufLen = 0; // Length of readBuf[] used area /* Index in buffer where the scanner should stop scanning and return * S_EOT to caller. On these places there will be a mark placed. */ int pos = 0; // pos in whole document int line = 0; // line counter for marks int preScan = 0; // prescan needed by syntax scanner int lineLimit = 0; // longest line found int lineLimitLastOffset = 0; synchronized (doc.op) { // array for getting mark array from renderer inner class final Mark origMarks[][] = new Mark[1][]; doc.op.renderMarks(new DocMarks.Renderer() { public void render() { int markCnt = getMarkCnt(); origMarks[0] = new Mark[markCnt]; System.arraycopy(getMarkArray(), 0, origMarks[0], 0, markCnt); } }); // now remove all the marks that are not insert after for (int i = 0; i < origMarks[0].length; i++) { Mark mark = origMarks[0][i]; if (!(mark.getInsertAfter() || (mark instanceof MarkFactory.CaretMark)) ) { try { mark.remove(); } catch (InvalidMarkException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } } } // Enter the loop where all data from file will be read while (true) { // check preScan and possibly copy some chars from end of previous buffer preScan = syntax.getPreScan(); // get current preScan for syntax scanner if (preScan > 0) { if (preScan > readBuf.length / 2) { // prescan bigger than half of buffer char[] tmp = new char[2 * readBuf.length + preScan]; // extend read buffer System.arraycopy(readBuf, bufLen - preScan, tmp, 0, preScan); readBuf = tmp; } else { // the same buffer System.arraycopy(readBuf, bufLen - preScan, readBuf, 0, preScan); } } // first check if lastCR is set (preScan must be already set // either to 0 for first read or at end of body of while() cycle) if (lastCR == 1) { // last char in previous buffer was CR readBuf[preScan] = '\r'; } // read part of document into buffer readLen = 0; while (readLen == 0) { // read non-zero chars for algorithm to work readLen = reader.read(readBuf, preScan + lastCR, readBuf.length - 2 - preScan - lastCR); } // check readLen value if (readLen == -1) { // no more characters bufLen = preScan + lastCR; lastRead = true; } else { // some chars were read bufLen = preScan + lastCR + readLen; if (readBuf[bufLen - 1] == '\r') { // last char in buffer is '\r' bufLen--; // don't process last character thisCR = 1; // set that there's CR at the end of the buffer } } // check if we need to scan buffer for LS if (firstRead && testLS) { String newLS = testLS(readBuf, bufLen); if (newLS != null) { doc.putProperty(BaseDocument.READ_LINE_SEPARATOR_PROP, newLS); if (doc.getProperty(BaseDocument.WRITE_LINE_SEPARATOR_PROP) == null) { doc.putProperty(BaseDocument.WRITE_LINE_SEPARATOR_PROP, newLS); } } } // convert the line separators strictly to buffer bufLen = convertLSToLF(readBuf, bufLen); // set scanning indexes if (firstRead) { // for first read set scanning index syntax.load(null, readBuf, preScan, bufLen - preScan, false); } else { // for next reads only relocate scanning syntax.relocate(readBuf, preScan, bufLen - preScan, false); } // now handle whole buffer - do syntax analyzes and create marks int bufOffset = preScan; while (bufOffset < bufLen) { // compute stop index syntax.setStopOffset(bufOffset = Math.min(bufOffset + markDistance, bufLen)); boolean cont = true; while (cont) { switch (syntax.nextToken()) { // ignore regular tokens case Syntax.EOL: // end of line found int offset = syntax.getOffset(); lineLimit = Math.max(lineLimit, offset - lineLimitLastOffset); lineLimitLastOffset = offset; line++; // increase line counter break; case Syntax.EOT: // stop index reached MarkFactory.SyntaxMark mark = new MarkFactory.SyntaxMark(); mark.updateStateInfo(syntax); try { doc.op.insertMark(mark, pos + bufOffset - preScan, line); } catch (BadLocationException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } catch (InvalidMarkException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } cont = false; // break the while() break; } } } lineLimitLastOffset -= syntax.tokenOffset; // store this buffer into cacheSupport try { doc.op.directCacheWrite(pos, readBuf, preScan, bufLen - preScan); } catch (BadLocationException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } // initialize cache with first buffer if (firstRead) { doc.op.initCacheContent(readBuf, preScan, bufLen - preScan); } pos += bufLen - preScan; lastCR = thisCR; thisCR = 0; firstRead = false; // must be false after first cycle if (lastRead) { // break while cycle if at last read break; } } // Now reinsert marks that were removed at begining to the end for (int i = 0; i < origMarks[0].length; i++) { Mark mark = origMarks[0][i]; if (!(mark.getInsertAfter() || (mark instanceof MarkFactory.CaretMark)) ) { try { doc.op.insertMark(origMarks[0][i], pos, line); } catch (InvalidMarkException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } catch (BadLocationException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } } } // Set the line limit document property doc.putProperty(BaseDocument.LINE_LIMIT_PROP, new Integer(lineLimit)); doc.op.initialReadUpdate(); } } } /** Read from some reader and insert into document */ static void read(BaseDocument doc, Reader reader, int pos) throws BadLocationException, IOException { int lastCR = 0; int thisCR; boolean lastRead = false; int readLen; int readBufferSize = ((Integer)doc.getProperty( Settings.READ_BUFFER_SIZE)).intValue(); char[] readBuf = new char[readBufferSize + 1]; while (true) { // read part of document into buffer readLen = 0; while (readLen == 0) { // read non-zero chars for algorithm to work readLen = reader.read(readBuf, lastCR, readBufferSize); } thisCR = 0; if (readLen == -1) { if (lastCR == 0) { break; } else { readLen = 0; } } else { // some chars were read if (readBuf[readLen + lastCR - 1] == '\r') { thisCR = 1; readLen--; } } readLen += lastCR; readLen = convertLSToLF(readBuf, readLen); doc.insertString(pos, new String(readBuf, 0, readLen), null); pos += readLen; lastCR = thisCR; } } /** Write from document to some writer */ static void write(BaseDocument doc, Writer writer, int pos, int len) throws BadLocationException, IOException { String lsType = (String)doc.getProperty(BaseDocument.WRITE_LINE_SEPARATOR_PROP); if (lsType == null) { lsType = (String)doc.getProperty(BaseDocument.READ_LINE_SEPARATOR_PROP); if (lsType == null) { lsType = BaseDocument.LS_LF; } } int writeBufferSize = ((Integer)doc.getProperty( Settings.WRITE_BUFFER_SIZE)).intValue(); char[] getBuf = new char[writeBufferSize]; char[] writeBuf = new char[2 * writeBufferSize]; while (len > 0) { int actLen = Math.min(len, writeBufferSize); doc.getChars(pos, getBuf, 0, actLen); int tgtLen = convertLFToLS(getBuf, actLen, writeBuf, lsType); writer.write(writeBuf, 0, tgtLen); pos += actLen; len -= actLen; } } /** Get visual column. */ public static int getColumn(char buffer[], int offset, int len, int tabSize, int startCol) { int col = startCol; int endOffset = offset + len; while (offset < endOffset) { switch (buffer[offset++]) { case '\t': col = (col + tabSize) / tabSize * tabSize; break; default: col++; } } return col; } /** Get buffer filled with appropriate number of spaces. The buffer * can have actually more spaces than requested. * @param numSpaces number of spaces */ public static char[] getSpacesBuffer(int numSpaces) { // check if there's enough space in white space array if (numSpaces > spacesBuffer.length) { char tmpBuf[] = new char[numSpaces * 2]; // new buffer // initialize new buffer with spaces for (int i = 0; i < tmpBuf.length; i += spacesBuffer.length) { System.arraycopy(spacesBuffer, 0, tmpBuf, i, Math.min(spacesBuffer.length, tmpBuf.length - i)); } spacesBuffer = tmpBuf; } return spacesBuffer; } public static char[] createSpacesBuffer(int numSpaces) { char[] ret = new char[numSpaces]; System.arraycopy(getSpacesBuffer(numSpaces), 0, ret, 0, numSpaces); return ret; } /** Get buffer filled with appropriate number of tabs. The buffer * can have actually more tabs than requested. * @param numSpaces number of spaces */ public static char[] getTabsBuffer(int numTabs) { // check if there's enough space in white space array if (numTabs > tabsBuffer.length) { char tmpBuf[] = new char[numTabs * 2]; // new buffer // initialize new buffer with spaces for (int i = 0; i < tmpBuf.length; i += tabsBuffer.length) { System.arraycopy(tabsBuffer, 0, tmpBuf, i, Math.min(tabsBuffer.length, tmpBuf.length - i)); } tabsBuffer = tmpBuf; } return tabsBuffer; } public static char[] getIndentChars(int indent, boolean expandTabs, int tabSize) { if (expandTabs) { return createSpacesBuffer(indent); } else { return createWhiteSpaceFillBuffer(0, indent, tabSize); } } /** Get buffer filled with spaces/tabs so that it reaches from * some column to some other column. */ public static char[] createWhiteSpaceFillBuffer(int startCol, int endCol, int tabSize) { if (startCol >= endCol) { return EMPTY_CHAR_ARRAY; } int tabs = 0; int spaces = 0; int nextTab = (startCol + tabSize) / tabSize * tabSize; if (nextTab > endCol) { // only spaces spaces += endCol - startCol; } else { // at least one tab tabs++; // jump to first tab int endSpaces = endCol - endCol / tabSize * tabSize; tabs += (endCol - endSpaces - nextTab) / tabSize; spaces += endSpaces; } char[] ret = new char[tabs + spaces]; if (tabs > 0) { System.arraycopy(getTabsBuffer(tabs), 0, ret, 0, tabs); } if (spaces > 0) { System.arraycopy(getSpacesBuffer(spaces), 0, ret, tabs, spaces); } return ret; } /** Loads the file and performs conversion of line separators to LF. * This method can be used in debuging of syntax scanner or somewhere else. * @param fileName the name of the file to load * @return array of loaded characters with '\n' as line separator */ public static char[] loadFile(String fileName) throws IOException { File file = new File(fileName); char chars[] = new char[(int)file.length()]; FileReader reader = new FileReader(file); reader.read(chars); reader.close(); int len = Analyzer.convertLSToLF(chars, chars.length); if (len != chars.length) { char copyChars[] = new char[len]; System.arraycopy(chars, 0, copyChars, 0, len); chars = copyChars; } return chars; } /** Convert text with LF line separators to text that uses * line separators of the document. This function is used when * saving text into the file. Segment's data are converted inside * the segment's data or new segment's data array is allocated. * NOTE: Source segment must have just LFs as separators! Otherwise * the conversion won't work correctly. * @param src source chars to convert from * @param len length of valid part of src data * @param tgt target chars to convert to. The array MUST have twice * the size of src otherwise index exception can be thrown * @param lsType line separator type to be used i.e. LS_LF, LS_CR, LS_CRLF * @return length of valid chars in tgt array */ public static int convertLFToLS(char[] src, int len, char[] tgt, String lsType) { if (lsType.equals(BaseDocument.LS_CR)) { // CR instead of LF System.arraycopy(src, 0, tgt, 0, len); // now do conversion for LS_CR if (lsType == BaseDocument.LS_CR) { // will convert '\n' to '\r' char chars[] = tgt; for (int i = 0; i < len; i++) { if (chars[i] == '\n') { chars[i] = '\r'; } } } return len; } else if (lsType.equals(BaseDocument.LS_CRLF)) { int tgtLen = 0; int moveStart = 0; // start of block that must be moved int moveLen; // length of chars moved for (int i = 0; i < len; i++) { if (src[i] == '\n') { // '\n' found moveLen = i - moveStart; if (moveLen > 0) { // will need to arraycopy System.arraycopy(src, moveStart, tgt, tgtLen, moveLen); tgtLen += moveLen; } tgt[tgtLen++] = '\r'; tgt[tgtLen++] = '\n'; moveStart = i + 1; // skip separator } } // now move the rest if it's necessary moveLen = len - moveStart; if (moveLen > 0) { System.arraycopy(src, moveStart, tgt, tgtLen, moveLen); tgtLen += moveLen; } return tgtLen; } else { // Using either \n or line separator is unknown System.arraycopy(src,0, tgt, 0, len); return len; } } public static boolean startsWith(char[] chars, char[] prefix) { if (chars == null || chars.length < prefix.length) { return false; } for (int i = 0; i < prefix.length; i++) { if (chars[i] != prefix[i]) { return false; } } return true; } public static boolean endsWith(char[] chars, char[] suffix) { if (chars == null || chars.length < suffix.length) { return false; } for (int i = chars.length - suffix.length; i < chars.length; i++) { if (chars[i] != suffix[i]) { return false; } } return true; } public static char[] concat(char[] chars1, char[] chars2) { if (chars1 == null || chars1.length == 0) { return chars2; } if (chars2 == null || chars2.length == 0) { return chars1; } char[] ret = new char[chars1.length + chars2.length]; System.arraycopy(chars1, 0, ret, 0, chars1.length); System.arraycopy(chars2, 0, ret, chars1.length, chars2.length); return ret; } public static char[] extract(char[] chars, int offset, int len) { char[] ret = new char[len]; System.arraycopy(chars, offset, ret, 0, len); return ret; } public static boolean blocksHit(int[] blocks, int startPos, int endPos) { return (blocksIndex(blocks, startPos, endPos) >= 0); } public static int blocksIndex(int[] blocks, int startPos, int endPos) { if (blocks.length > 0) { int onlyEven = ~1; int low = 0; int high = blocks.length - 2; while (low <= high) { int mid = ((low + high) / 2) & onlyEven; if (blocks[mid + 1] <= startPos) { low = mid + 2; } else if (blocks[mid] >= endPos) { high = mid - 2; } else { return low; // found } } } return -1; } /** Remove all spaces fromt the given string */ public static String removeSpaces(String s) { int spcInd = s.indexOf(' '); while (spcInd >= 0) { s = s.substring(0, spcInd) + s.substring(spcInd).trim(); spcInd = s.indexOf(' '); } return s; } } /* * Log * 41 Gandalf-post-FCS1.38.1.1 4/3/00 Miloslav Metelka undo update * 40 Gandalf-post-FCS1.38.1.0 3/8/00 Miloslav Metelka * 39 Gandalf 1.38 1/13/00 Miloslav Metelka * 38 Gandalf 1.37 1/10/00 Miloslav Metelka * 37 Gandalf 1.36 1/6/00 Miloslav Metelka Fixed #4584 * 36 Gandalf 1.35 12/28/99 Miloslav Metelka * 35 Gandalf 1.34 11/14/99 Miloslav Metelka * 34 Gandalf 1.33 11/8/99 Miloslav Metelka * 33 Gandalf 1.32 10/23/99 Ian Formanek NO SEMANTIC CHANGE - Sun * Microsystems Copyright in File Comment * 32 Gandalf 1.31 10/10/99 Miloslav Metelka * 31 Gandalf 1.30 9/16/99 Miloslav Metelka * 30 Gandalf 1.29 9/15/99 Miloslav Metelka * 29 Gandalf 1.28 9/10/99 Miloslav Metelka * 28 Gandalf 1.27 8/27/99 Miloslav Metelka * 27 Gandalf 1.26 7/29/99 Miloslav Metelka * 26 Gandalf 1.25 7/26/99 Miloslav Metelka * 25 Gandalf 1.24 7/22/99 Miloslav Metelka * 24 Gandalf 1.23 7/21/99 Miloslav Metelka * 23 Gandalf 1.22 7/21/99 Miloslav Metelka * 22 Gandalf 1.21 7/20/99 Miloslav Metelka * 21 Gandalf 1.20 7/9/99 Miloslav Metelka * 20 Gandalf 1.19 7/2/99 Miloslav Metelka * 19 Gandalf 1.18 6/24/99 Miloslav Metelka Drawing improved * 18 Gandalf 1.17 6/10/99 Miloslav Metelka * 17 Gandalf 1.16 6/8/99 Miloslav Metelka * 16 Gandalf 1.15 6/1/99 Miloslav Metelka * 15 Gandalf 1.14 6/1/99 Miloslav Metelka * 14 Gandalf 1.13 5/21/99 Miloslav Metelka * 13 Gandalf 1.12 5/18/99 Miloslav Metelka fixed loading large * files * 12 Gandalf 1.11 5/5/99 Miloslav Metelka * 11 Gandalf 1.10 4/23/99 Miloslav Metelka changes in settings * 10 Gandalf 1.9 4/23/99 Miloslav Metelka Undo added and internal * improvements * 9 Gandalf 1.8 4/6/99 Miloslav Metelka fixed #1437 * 8 Gandalf 1.7 4/1/99 Miloslav Metelka * 7 Gandalf 1.6 3/30/99 Miloslav Metelka * 6 Gandalf 1.5 3/27/99 Miloslav Metelka * 5 Gandalf 1.4 3/23/99 Miloslav Metelka * 4 Gandalf 1.3 3/18/99 Miloslav Metelka * 3 Gandalf 1.2 2/9/99 Miloslav Metelka * 2 Gandalf 1.1 2/3/99 Miloslav Metelka * 1 Gandalf 1.0 2/3/99 Miloslav Metelka * $ */